S02-04 面向对象-静态成员
[TOC]
static
在 Java 中,static 是一个出场率极高、也是极其重要的关键字。
如果用一句话来概括 static 的核心思想,那就是:“属于类,而不属于对象”。 它打破了面向对象中“一切皆需实例化”的常规,提供了一种全局共享的机制。在 Java 中,static 关键字主要有 5 种 使用场景。我们将它们梳理成一份完整的“全景图”。
核心特性@
深入剖析 static 的核心特点是非常有必要的!如果说上一节我们看的是 static “能干什么(应用场景)”,那么这一节我们来透视它 “本质上是什么(核心特点)”。
理解了这几个核心特点,以后遇到任何关于 static 的报错或面试题,你都能一眼看穿其底层逻辑。
我们可以把 static 的核心特点总结为以下 5 个维度:
归属性:属于类,而非属于对象:
这是
static最根本的特点,也是其他所有特点的基础。- 普通成员: 是“私有财产”。必须先有具体的对象(比如先造出一辆具体的特斯拉),才能讨论它的颜色、速度。
static成员: 是“公共设施”或“图纸属性”。它直接绑定在“类”这图纸上。即使你一个对象都没有创建,static属性和方法也已经存在,并且可以直接通过类名.成员名来使用。
唯一性:内存中只有一份拷贝:
不管你用这个类
new出了 1 个对象,还是 10000 个对象,被static修饰的变量在内存中永远只有一份。- 内存位置: 它不存放在专门存放对象的堆内存(Heap)中,而是存放在方法区(Method Area / 现代 JVM 称之为 Metaspace 元空间) 的静态存储区里。
- 现实比喻: 就像一个班级(类)里有 50 个学生(对象)。每个学生都有自己的课本(实例变量),但教室前面只有一块黑板(
static变量)。黑板只有一块,所有人看的内容都是一样的。
先后性:初始化时机极早:
static成员的出生时间比对象要早得多。- 加载时机: 当 JVM 第一次把这个类的字节码(
.class文件)加载到内存时,static变量就会被分配内存并初始化,static代码块也会立刻执行。 - 底层逻辑推演: 因为它生得太早了,这时候堆内存里根本还没有任何该类的对象。这就引出了下面第 4 个、也是程序员最容易踩坑的特点。
- 加载时机: 当 JVM 第一次把这个类的字节码(
局限性:严格的访问壁垒:
这是初学者最容易遇到编译报错的地方:“静态方法中不能引用非静态成员”。
静态禁止访问非静态:
static方法内部,绝对不能直接访问实例变量,也不能调用实例方法,更不能使用this和super关键字。- 为什么? 用时间线来解释很简单:
static成员出生的时候,对象连个影子都没有(没有this)。你让一个早早就出生的“静态爷爷”,去叫一个还没出生的“对象孙子”的名字,他上哪找去?

- 为什么? 用时间线来解释很简单:
非静态允许访问静态: 反过来完全没问题。普通对象创建的时候,
static成员早就存在于内存中了,对象随时可以去访问这块“公共黑板”。
共享与风险性:全局状态的并发灾难:
因为唯一,所以共享;因为共享,所以危险。
- 一处修改,处处改变: 任何一个对象修改了
static变量的值,其他所有对象看到的都会是修改后的最新值。 - 线程不安全: 在多线程(Web 后端开发最常见)环境下,如果多个线程同时去读写同一个
static变量,极其容易发生数据覆盖、脏读等线程安全问题 (Race Condition)。 - 这就是为什么在业务开发中,除了定义常量(
static final)或者无状态的工具方法外,我们极其反感定义全局的static变量。
- 一处修改,处处改变: 任何一个对象修改了
五大应用场景
五大应用场景:
静态属性
在 Java 中,静态属性(静态变量,静态字段,Static Field / Variable) 是用 static 关键字修饰的成员变量。
理解静态属性的关键在于一句话:它属于“类”本身,而不属于某个具体的“对象”。
独享 vs 共享
核心概念:独享 vs 共享:
为了理解静态属性,我们先对比一下它和普通(实例)属性的区别。
实例属性 (Instance Variable):
- 拥有者: 对象。
- 比喻: 每个人手中的水杯。张三喝了一口他杯子里的水,李四杯子里的水不会变少。每个对象都有自己独立的一份拷贝。
静态属性 (Static Variable):
- 拥有者: 类。
- 比喻: 办公室里的饮水机。它是全公司公用的。如果张三把饮水机的水接干了,李四去接水时,饮水机也是干的。所有对象共享同一份数据。
语法与使用
定义方式:在变量类型前加上 static 关键字。
public class Student {
// 实例属性:每个学生的名字不一样
String name;
// 静态属性:所有学生都在同一个学校
static String school = "清华大学";
public Student(String name) {
this.name = name;
}
}访问方式:虽然你可以用对象去访问静态属性,但这是一种不推荐的写法。推荐直接使用类名访问。
public class Main {
public static void main(String[] args) {
Student s1 = new Student("张三");
Student s2 = new Student("李四");
// 1. 推荐:通过类名访问
System.out.println(Student.school); // 输出:清华大学
// 2. 不推荐:通过对象访问 (虽然语法允许,但容易引起误解)
System.out.println(s1.school); // 输出:清华大学
// 3. 修改静态属性:牵一发而动全身
Student.school = "北京大学";
System.out.println(s1.school); // 输出:北京大学
System.out.println(s2.school); // 输出:北京大学 (s2 也跟着变了!)
}
}快速入门
入门需求:一群小孩玩堆雪人,不时有新小孩加入,如何统计当前玩游戏的总人数?
传统方式实现:
javapublic class ChildGame { public static void main(String[] args) { // 1. 定义计数器 public int count = 0; // 2. 每创建一个小孩,计数器 +1 Child child1 = new Child("白骨精"); child1.join(); count++; Child child2 = new Child("狐狸精"); child2.join(); count++; Child child3 = new Child("老鼠精"); child3.join(); count++; // 3. 打印小孩总数 System.out.println("共有" + Child.count + " 小孩加入了游戏..."); } }javaclass Child { private String name; public Child(String name) { this.name = name; } public void join() { System.out.println(name + " 加入了游戏.."); } }- 问题:
count是定义的独立变量,与 Child 并没有关系,不遵循 OOP 思想- 访问和维护麻烦,不共享于对象
- 问题:
静态属性实现:
javapublic class ChildGame { public static void main(String[] args) { Child child1 = new Child("白骨精"); Child child2 = new Child("狐狸精"); Child child3 = new Child("老鼠精"); child1.join(); child2.join(); child3.join(); // 3. 访问静态属性(类名.静态属性,推荐) System.out.println("共有" + Child.count + " 小孩加入了游戏..."); // 不推荐通过对象访问静态属性 System.out.println("child1.count=" + child1.count); // 3 System.out.println("child2.count=" + child2.count); // 3 System.out.println("child3.count=" + child3.count); // 3 } }javaclass Child { private String name; // 1. 定义静态属性(所有实例对象共享) public static int count = 0; public Child(String name) { this.name = name; } public void join() { System.out.println(name + " 加入了游戏.."); // 2. 修改静态属性(自增) count++; } }
内存原理@
内存原理 (Under the Hood):
理解内存模型是掌握静态属性的关键。
- 存储位置:
- 实例属性 存储在 堆内存 (Heap) 中,跟随对象的创建而分配。
- 静态属性:
- Java 8 之前:存储在方法区 (Method Area)的静态区 中。
- Java 8 及之后:存储在类加载时堆内存中生成的 Class 实例的尾部(在 Java 8+ 称为元空间 Metaspace)。
- 生命周期:
- 出生: 随着类加载 (Class Loading) 而创建。这意味着,即使你没有
new任何对象,静态属性也可以使用了! - 死亡: 随着 JVM 关闭或类被卸载而销毁(通常伴随整个程序的生命周期)。
- 出生: 随着类加载 (Class Loading) 而创建。这意味着,即使你没有

应用场景
什么时候使用静态属性 (常见场景):
静态属性不能滥用,通常只在以下三种场景使用:
共享变量 / 计数器:
当你希望所有对象共享同一个状态时。
示例: 统计一共创建了多少个对象。
javaclass User { // 1. 静态计数器 public static int count = 0; public User() { // 2. 每次创建对象,计数器 +1 count++; } } // 3. User.count 的值就是当前创建的对象总数
全局常量 (Constants):
这是静态属性最常用的场景,通常配合
final关键字一起使用。格式:
public static final 类型 常量名 = 值;示例:
Math.PI,或者项目配置。javapublic class AppConfig { // 数据库 URL,全局唯一且不可变 public static final String DB_URL = "jdbc:mysql://localhost:3306/mydb"; }
工具类的数据:
如果一个类只是提供工具方法(如
Math类),通常不需要创建实例,其属性也会设为static。
静态代码块
静态代码块 (Static Initialization Block):
如果你需要对静态属性进行复杂的初始化(比如读取配置文件、循环赋值),可以使用静态代码块。
特点: 在类加载时自动执行,且只执行一次。
javaclass Driver { static String driverVersion; // 静态代码块 static { System.out.println("类加载了,初始化驱动..."); // 模拟复杂的初始化逻辑 driverVersion = "v1.0.0"; } } // 第一次访问 Driver 类时,会先打印 "类加载了..."
静态属性 vs 实例属性
| 特性 | 静态属性 (Static Variable) | 实例属性 (Instance Variable) |
|---|---|---|
| 关键字 | static | 无 |
| 所属 | 类 (Class) | 对象 (Object) |
| 内存位置 | 方法区 (Method Area) | 堆内存 (Heap) |
| 副本数量 | 只有一份 (共享) | 每个对象一份 (独立) |
| 生命周期 | 长 (类加载 -> 程序结束) | 短 (对象创建 -> 垃圾回收) |
| 调用方式 | 类名.变量名 (推荐) | 对象名.变量名 |
注意事项
致命陷阱与注意事项:
虽然静态属性好用,但它是 Bug 的温床,一定要小心:
线程安全问题 (Thread Safety):
- 因为所有线程共享同一个静态变量,如果多个线程同时修改它,会导致数据错乱(Race Condition)。
- 解决: 尽量使用
static final(不可变),或者在修改时加锁 (synchronized)。
内存泄漏 (Memory Leak):
- 静态变量的生命周期非常长。如果你在一个
static List中不断添加对象而不删除,这些对象永远不会被垃圾回收(GC),最终导致内存溢出 (OOM)。
- 静态变量的生命周期非常长。如果你在一个
测试困难:
- 在单元测试中,静态变量的状态会保留到下一个测试用例中,导致测试之间相互干扰(脏数据)。
面试:System.out.println
经典面试题::System.out.println
我们每天都在写的这行代码,其实就是静态属性的完美应用:
System: 是一个类 (java.lang.System)。out: 是System类中的一个 静态属性 (public static final PrintStream out)。println: 是out这个对象(PrintStream类)里的方法。
所以我们不需要 new System() 就能直接用,就是因为 out 是静态的。
静态方法
静态方法(Static Method) 是 static 关键字的另一半壁江山。静态方法是属于类的方法,而不是属于对象的方法。
工具箱 vs 个人技能
核心概念:工具箱 vs 个人技能:
为了直观理解,我们可以打个比方:
实例方法 (Instance Method): 就像是“唱歌”。
- 这是一个个人技能。你必须先找到一个具体的“人”(对象),比如张三,然后让他唱歌。没人就没法唱。
- 调用方式:
zhangSan.sing()。
静态方法 (Static Method): 就像是“加法运算”。
- 这是一个通用工具。你不需要找一个特定的人来算 。这个逻辑是通用的,存在于“数学规则”(类)中。
- 调用方式:
Math.add(1, 1)。
语法与调用
定义:在方法返回值类型前加上 static 关键字。
public class Calculator {
// 静态方法:不需要创建对象就能用
public static int add(int a, int b) {
return a + b;
}
// 实例方法:必须创建对象才能用
public void sayHello() {
System.out.println("Hello World");
}
}调用:原则:推荐直接使用类名调用。
public class Main {
public static void main(String[] args) {
// 1. 调用静态方法 (标准写法)
int result = Calculator.add(10, 20);
// 2. 调用实例方法 (必须先 new)
Calculator c = new Calculator();
c.sayHello();
// 3. 用对象调用静态方法 (不推荐,会被 IDE 警告)
// c.add(10, 20); // 虽然能跑,但误导性强,以为它跟对象 c 有关
}
}原因:反编译后本质还是通过类名调用的静态方法。

快速入门
快速入门:静态方法 + 静态属性
public class Main {
public static void main(String[] args) {
// 4. 静态方法无需创建对象即可调用
Student.payFee(100); // 可以使用对象访问静态方法,但不推荐
Student.payFee(200);
Student.showFee(); // 总学费有:300
}
}
class Student {
private String name;
// 1. 静态变量:保存累积学费
private static double fee = 0;
public Student(String name) {
this.name = name;
}
// 2. 静态方法:访问静态变量并自增
public static void payFee(double fee) {
Student.fee += fee;
}
// 3. 静态方法:显示总学费
public static void showFee() {
System.out.println("总学费有:" + Student.fee);
}
}核心特性@
核心特性::
这是面试和开发中最重要的规则,静态方法由于没有“对象”的上下文,所以受到严格限制:
不准直接访问非静态成员变量:
原因: 静态方法加载时,对象可能还没出生呢。
javaclass User { String name; // 实例变量 static String school; // 静态变量 public static void test() { // System.out.println(name); // ❌ 编译错误!我怎么知道是哪个 User 的名字? System.out.println(school); // ✅ 可以,因为学校是大家共用的 } }不准直接调用非静态方法:
原因: 同上。
javapublic void run() { ... } public static void test() { // run(); // ❌ 编译错误!run 需要依附于对象 }注意:如果不直接调用,而是自己在静态方法里
new了一个对象,那是可以调用该对象的实例方法的。不准使用
this和super关键字:原因:
this代表“当前对象”。静态方法执行时,根本就没有“当前对象”这个概念。静态方法可以被继承,不能被重写:
这是一个经典的面试陷阱:静态方法可以被继承,但不能被“重写” (Override)。
如果在子类中定义了和父类一模一样的静态方法,这叫 “隐藏” (Hiding)。
javaclass Father { public static void talk() { System.out.println("爸爸在说话"); } } class Son extends Father { // 1. 这不是重写,这是“隐藏”了父类的方法 public static void talk() { System.out.println("儿子在说话"); } } public class Main { public static void main(String[] args) { // 2. 看起来是多态,实际上调用的是 Father 的静态方法! Father f = new Son(); f.talk(); // 爸爸在说话 } }输出结果:
爸爸在说话原理解析:
- 重写 (Override) 是运行时多态,看对象是谁(右边
new的是谁)。 - 静态方法 是编译时绑定,看引用类型是谁(左边定义的变量类型是谁)。因为
f被定义为Father类型,所以直接调用Father.talk()。
- 重写 (Override) 是运行时多态,看对象是谁(右边
应用场景
三大应用场景:
通常在以下三种场景下,我们会把方法设计为 static:
工具类 (Utility Classes):
这是最常见的用法。如果一个方法完全独立于对象的状态,只根据传入的参数计算结果,它就应该是静态的。
- 例子:
java.lang.Math(算数)、java.util.Arrays(数组排序)、java.util.Collections。 - 代码:
Math.max(10, 20)。

- 例子:
main 方法:
程序的入口。
javapublic static void main(String[] args) { ... }为什么 main 必须是 static?
因为在程序启动时,JVM 还不知道该创建哪个对象。只有设为
static,JVM 才能直接通过类名.main()来启动程序。工厂方法 (Factory Methods):
用于替代构造函数创建对象,通常命名为
valueOf,getInstance等。- 例子:
LocalDate.now()。
- 例子:
静态方法 vs 实例方法
| 特性 | 静态方法 (Static) | 实例方法 (Instance) |
|---|---|---|
| 关键字 | static | 无 |
| 归属 | 类 | 对象 |
| 调用 | ClassName.method() | obj.method() |
| 访问权限 | 只能访问静态成员 | 可以访问静态 + 非静态成员 |
关键字 this | 不可用 ❌ | 可用 ✅ |
| 多态性 | 不可重写 (隐藏) | 可以重写 (多态) |
| 常见用途 | 工具类、工厂、入口 | 业务逻辑、对象行为 |
练习题
练习:静态属性
javapublic class Test { static int count = 9; public void count() { System.out.println("count=" + (count++)); } public static void main(String args[]) { new Test().count(); // 输出:9 new Test().count(); // 输出:10 System.out.println(Test.count); // 输出:11 } }练习:静态方法 + 静态属性(静态方法中不能访问非静态成员)
javaclass Person { private int id; private static int total = 0; // 静态方法:返回总人数 public static int getTotalPerson() { // id++;// ❌ 错误:静态方法不能访问非静态变量 return total; } // 构造器 public Person() { total++; // 总人数自增 id = total; // 给id赋值 } } public class TestPerson { public static void main(String[] args) { System.out.println("Number of total is " + Person.getTotalPerson()); // 0 Person p1 = new Person(); // total -> 1; id -> 1 System.out.println("Number of total is " + Person.getTotalPerson()); // 1 } }练习:静态方法 + 静态属性(静态方法中不能使用 this)
javaclass Person { private int id; private static int total = 0; // 静态方法:设置总人数 public static void setTotalPerson(int total) { // this.total = total; // ❌ 错误:静态方法不能使用 this Person.total = total; } public Person() { // 构造器 total++; id = total; } } public class TestPerson { public static void main(String[] args) { Person.setTotalPerson(3); // total -> 3 new Person(); // total -> 4 } }
静态代码块
在 Java 中,静态代码块(Static Initialization Block) 是一个非常特殊且强大的代码区域。
它的标志是用 static 关键字修饰的一对大括号 { ... }。
核心作用:它在类被加载(Class Loading)时自动执行,且在整个程序运行期间,只执行一次。
它通常用于初始化复杂的静态数据(如读取配置文件、加载数据库驱动、填充静态 Map 等)。
基本语法
基本语法:
public class Demo {
// 静态变量
static int number;
static ArrayList<String> list = new ArrayList<>();
// 【静态代码块】
static {
System.out.println("静态代码块被执行了!");
number = 100;
list.add("初始化数据A");
list.add("初始化数据B");
}
public static void main(String[] args) {
System.out.println("main 方法执行");
System.out.println("number = " + number);
}
}输出结果:
静态代码块被执行了!
main 方法执行
number = 100注意观察: 静态代码块甚至在 main 方法的第一行代码之前就执行了(因为访问 main 方法所在的类会触发类的加载)。
核心特性
执行时机:类加载时:
- 静态代码块不依赖于对象的创建。
- 只要这个类被 JVM 加载(第一次用到该类时,比如
new对象、访问静态属性、调用静态方法,或者Class.forName()),静态代码块就会立即执行。
执行频率:仅此一次:
- 无论你
new了多少个对象,静态代码块只在第一次加载类时运行一次。 - 这非常适合用来做“全局只需做一次”的初始化工作。
- 无论你
只能访问静态成员:
- 在静态代码块中,你不能访问非静态的成员变量(实例变量)或实例方法。
- 你也不能使用
this或super关键字。
执行顺序
执行顺序(继承关系中):
这是 Java 笔试中最容易晕头转向的题目:静态块、构造代码块、构造方法的执行顺序。
原则:
静态优于非静态: 父类静态 -> 子类静态。
父类优于子类: 父类初始化 -> 子类初始化。
构造块优于构造器: 实例代码块 -> 构造函数。
代码实战:
class Father {
static { System.out.println("1. 父类静态代码块"); }
{ System.out.println("3. 父类构造代码块 (非静态)"); }
public Father() { System.out.println("4. 父类构造方法"); }
}
class Son extends Father {
static { System.out.println("2. 子类静态代码块"); }
{ System.out.println("5. 子类构造代码块 (非静态)"); }
public Son() { System.out.println("6. 子类构造方法"); }
}
public class Test {
public static void main(String[] args) {
System.out.println("--- 准备创建对象 ---");
new Son();
System.out.println("--- 再次创建对象 ---");
new Son();
}
}输出结果(请仔细核对):
1. 父类静态代码块 (类加载,只执行一次)
2. 子类静态代码块 (类加载,只执行一次)
--- 准备创建对象 ---
3. 父类构造代码块
4. 父类构造方法
5. 子类构造代码块
6. 子类构造方法
--- 再次创建对象 ---
3. 父类构造代码块
4. 父类构造方法
5. 子类构造代码块
6. 子类构造方法结论:
- 静态代码块最先执行,且不再重复。
- 构造代码块和构造方法每次创建对象都会执行。
应用场景
应用场景:
为什么我们需要它?直接在定义变量时赋值不就行了吗?
比如 static int a = 10;。
是的,简单赋值可以。但如果初始化的逻辑很复杂(需要 for 循环、异常处理、读取文件),就必须用静态代码块。
初始化静态 Map
场景 1:初始化静态 Map:
class ErrorCodes {
public static Map<Integer, String> map = new HashMap<>();
static {
// 复杂的 put 逻辑只能写在代码块里
map.put(404, "Not Found");
map.put(500, "Internal Server Error");
map.put(403, "Forbidden");
// 可能还需要从数据库读取更多错误码...
}
}JDBC 驱动加载
场景 2:JDBC 驱动加载 (经典案例):
在早期的 JDBC 编程中,我们经常看到这句话:
Class.forName("com.mysql.cj.jdbc.Driver");
这句话的作用就是触发 Driver 类的类加载。而在 Driver 类的源码内部,正是通过静态代码块将自己注册到驱动管理器的:
// com.mysql.cj.jdbc.Driver 源码片段
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
// 当类被加载时,把自己注册给 DriverManager
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
...
}总结与对比
总结与对比:
| 特性 | 静态代码块 (static {}) | 构造代码块 ({}) | 构造方法 (Constructor) |
|---|---|---|---|
| 关键字 | static 修饰 | 无修饰 | 类名相同 |
| 归属 | 类 | 对象 | 对象 |
| 执行时机 | 类加载时 | 创建对象时 (构造器前) | 创建对象时 |
| 执行次数 | 仅一次 | 每次 new 都执行 | 每次 new 都执行 |
| 用途 | 初始化静态资源、驱动加载 | 抽取构造器共性代码 (少用) | 初始化对象属性 |
静态内部类
在 Java 的嵌套类(Nested Classes)家族中,静态内部类(Static Inner Class) 是最特殊、也是在优秀开源框架源码中出场率最高的一位。
简单来说:它是一个定义在其他类内部的类,并且被 static 关键字修饰。
虽然它“寄人篱下”写在别的类里面,但它的行为表现其实更像是一个完全独立的普通类。
语法
语法与实例化方式:
静态内部类最大的特点是:它的创建,完全不需要依赖外部类的实例对象。
- 可以直接访问外部类的静态成员(即使是 private 的)
- 不能直接访问外部类的非静态(实例)成员
public class Outer {
private static String staticName = "外部类静态变量";
private String instanceName = "外部类实例变量";
// 【静态内部类】
public static class Inner {
public void display() {
// 1. 可以直接访问外部类的静态成员(即使是 private 的)
System.out.println("访问: " + staticName);
// 2. ❌ 编译报错!不能直接访问外部类的非静态(实例)成员
// System.out.println(instanceName);
}
}
}如何实例化(创建对象)?
因为它不依赖外部类对象,所以 new 的时候直接用 外部类.内部类 的形式即可。
public class Main {
public static void main(String[] args) {
// 不需要先 new Outer()!直接通过类名路径创建:
Outer.Inner innerObj = new Outer.Inner();
innerObj.display();
}
}核心特性
核心特性与铁律:
理解静态内部类,只要记住以下三条铁律:
没有“隐式引用” (No Hidden Reference):
普通的成员内部类在底层会自动持有一个外部类对象的引用(这就是为什么普通内部类能随意使用外部类的变量)。但静态内部类没有这个引用。这使得它非常干净、轻量,不容易造成内存泄漏。
只能访问静态成员:
既然它不持有外部类对象的引用,它自然就不知道外部类的实例变量是什么。所以,静态内部类内部只能直接访问外部类的
static变量和static方法。可以拥有各种成员:
静态内部类里面,既可以定义静态变量/方法,也可以定义非静态变量/方法。(而普通的成员内部类在 Java 16 之前是不允许定义静态成员的)。
应用场景
为什么要用静态内部类(三大黄金场景):
既然它表现得像个独立类,为什么不干脆把它拿出来单独建一个 .java 文件呢?主要有以下几个原因:
极致的封装与高内聚:
如果一个类 B 只为类 A 服务,把 B 放在 A 里面作为静态内部类,可以隐藏 B 的存在,让包结构更清晰。
Java 源码代表作:
HashMap.NodeHashMap内部用来存储键值对的节点类Node,就是一个静态内部类。因为除了HashMap,别人根本不需要用到Node。
Builder 建造者模式(最常用):
在构建包含几十个属性的复杂对象时,静态内部类是实现链式调用的最佳搭档。
javapublic class User { private String name; private int age; // 1. 私有化构造器,强迫使用 Builder private User(Builder builder) { this.name = builder.name; this.age = builder.age; } // 2. 静态内部类 Builder public static class Builder { private String name; private int age;public Builder setName(String name) { this.name = name; return this; // 返回当前 Builder 对象,实现链式调用 } public Builder setAge(int age) { this.age = age; return this; } public User build() { return new User(this); } } } // 3. 优雅的调用方式: User user = new User.Builder().setName("Gemini").setAge(3).build(); 单例模式(静态内部类实现法):
正如我们在“单例模式”中提到的,这是最优雅、安全的单例写法,利用了 JVM 类加载机制的互斥性,既实现了懒加载(Lazy Loading),又天然保证了线程安全。
javapublic class SingletonInner { // 1. 构造器设为 private private SingletonInner() {} // 2. 静态内部类,只有被调用时才会加载,加载时在类内部 new 一个实例对象,并且保证在内存中只有一个实例对象 private static class SingletonHolder { private static final SingletonInner INSTANCE = new SingletonInner(); } // 3. 对外暴露一个公共方法 public static SingletonInner getInstance() { return SingletonHolder.INSTANCE; } }java// 4. 调用公共方法 SingletonInner.getInstance()
静态内部类 vs 成员内部类
终极对比:静态内部类 vs 成员内部类:
| 维度 | 静态内部类 (static class) | 成员内部类 (class) |
|---|---|---|
| 外部类实例依赖 | 完全独立(不需要先创建外部类对象) | 强依赖(必须先 new 外部类对象,再 new 内部类) |
| 持有外部引用 | 不持有 (防内存泄漏) | 隐式持有 Outer.this |
| 访问外部成员 | 只能访问外部 static 成员 | 可以访问外部所有成员(包括私有实例变量) |
| 自身包含静态成员 | 可以包含任何成员 | Java 16 之前不能包含静态成员,之后可以 |
| 实例化语法 | new Outer.Inner() | outerObj.new Inner() |
静态导包
在 Java 中,静态导包(Static Import) 是 JDK 1.5(Java 5)引入的一个非常实用的“语法糖”。
如果用一句话来概括它的作用:它可以让你在调用某个类的静态变量或静态方法时,连“类名”都省略掉,直接写方法名或变量名。
这主要用于精简代码,让代码读起来更像自然语言或数学公式。
语法
我们先来看日常开发中最常见的例子:使用 java.lang.Math 类进行数学计算。
传统写法
哪怕我们已经导入了相关的类,每次调用静态方法时,还是必须带上 Math. 这个前缀。
// 1. 普通导包
import java.lang.Math;
public class NormalImportDemo {
public static void main(String[] args) {
// 2. 每次都要写 Math.
double r = Math.PI;
double area = Math.PI * Math.pow(r, 2);
int maxVal = Math.max(10, 20);
System.out.println("最大值是:" + maxVal);
}
}现代写法
在 import 后面加上 static 关键字,就可以把指定的静态成员直接“拉”到当前类的作用域里。
// 1. 【静态导包】导入 Math 类下的所有静态成员
import static java.lang.Math.*;
public class StaticImportDemo {
public static void main(String[] args) {
// 2. 直接使用 PI、pow 和 max,仿佛它们是这个类自己的方法一样!
double r = PI;
double area = PI * pow(r, 2);
int maxVal = max(10, 20);
System.out.println("最大值是:" + maxVal);
}
}情况分类
静态导包的两种精确度:
你可以选择导入具体的某一个方法/变量,也可以导入全部。
导入指定成员
导入指定成员(推荐):
如果你只需要用到一两个静态方法,建议精确导入,这样别人看代码时明确知道来源。
import static java.lang.Math.max;
import static java.lang.Math.PI;
// 此时你只能省略 max 和 PI 的前缀,如果用 Math.abs() 还是得写类名。导入所有静态成员
导入所有静态成员(用通配符 :*)
如果你在这个类里要大量用到某个工具类的各种方法,可以使用 *。
import static java.lang.Math.*;应用场景
核心应用场景(什么时候用):
虽然静态导包能省几下键盘敲击,但我们绝不会滥用它。在实际企业级开发中,它几乎只出现在以下三种经典场景:
单元测试 (Unit Testing) —— 最广泛的使用地:
在使用 JUnit 或 TestNG 写测试用例时,我们需要大量调用断言方法(Assert)。使用静态导包可以让测试代码极其清爽。
java// 静态导入所有的断言方法 import static org.junit.jupiter.api.Assertions.*; public class CalculatorTest { public void testAdd() { Calculator calc = new Calculator(); // 以前要写:Assertions.assertEquals(5, calc.add(2, 3)); // 现在直接写: assertEquals(5, calc.add(2, 3)); assertTrue(calc.isReady()); assertNotNull(calc); } }频繁使用常量的类:
如果你定义了一个全局常量类,在其他地方需要高频使用这些常量:
java// 常量类 package com.constant; public class Status { public static final int SUCCESS = 200; public static final int ERROR = 500; } // 业务类 import static com.constant.Status.*; public class BizService { public void process() { int code = SUCCESS; // 直接写 SUCCESS,而不是 Status.SUCCESS } }数学密集型计算:
当你编写复杂的算法,涉及大量的
sin(),cos(),abs(),pow()时,去掉Math.前缀能让公式更接近数学原本的样子,提升可读性。
注意事项
致命缺陷与注意事项(为什么不推荐滥用):
阿里 Java 开发手册及诸多编码规范中都明确指出:慎用静态导包。
原因只有两个字:歧义。
方法重名导致的混乱 (Namespace Pollution):
假设你静态导入了两个不同工具类中的所有方法,而这两个类恰好都有一个同名方法:
javaimport static java.util.Collections.*; import static java.util.Arrays.*; public class Test { public void doSomething() { // ❌ 报错! // Collections 里面有个 sort(),Arrays 里面也有个 sort() // 编译器彻底懵了,它不知道你到底想用哪一个。 // sort(list); } }严重降低代码可读性:
对于不熟悉你代码的人(或者几个月后的你自己)来说,突然在代码里看到一个没头没尾的
checkConfig()方法调用。你会本能地以为这是当前类定义的一个普通方法,于是按住
Ctrl找半天,最后才发现它竟然是从几十公里外的另一个包里“静态导入”过来的工具方法。这就极大地增加了阅读代码的认知负担。
总结
| 特性 | 传统导包 (import) | 静态导包 (import static) |
|---|---|---|
| 导入目标 | 类 (Class) 或 接口 (Interface) | 类的 静态变量 或 静态方法 |
| 调用方式 | 类名.方法名() | 直接写 方法名() |
| 优点 | 来源清晰,绝无重名歧义 | 代码极致精简,尤其适合测试断言和复杂数学公式 |
| 缺点 | 前缀重复,代码稍显冗长 | 滥用会导致来源不明,极易引发同名方法冲突 |
main 方法
main() 方法是 Java 应用程序的入口点 (Entry Point)。
它是 Java 程序执行的起点。当你运行一个 Java 程序时,JVM(Java 虚拟机) 会首先寻找这个特定的方法,并从这里开始一行一行地执行代码。
如果没有 main 方法,或者格式写错了,程序就无法启动,JVM 会抛出错误:Error: Main method not found in class...。
标准签名
标准签名 (The Standard Signature):
这是你必须背下来的“标准公式”。每一个单词都有其深刻的含义:
public static void main(String[] args) {
// 你的代码
}public(访问修饰符):- 含义: 公开的。
- 原因:
main方法是由 JVM 调用的。JVM 是一个“外部”程序,它不在你的类内部。为了让 JVM 能“看见”并调用这个方法,必须将其设置为public。(注:在 Java 9 之前的某些版本若不写 public 可能无法运行,但现在必须是 public)。
static(静态):- 含义: 静态的,属于类而非对象。
- 原因: 在程序启动时,内存中还没有任何对象。
- 如果
main不是static的,JVM 就必须先创建你的类的一个对象(new MyClass())才能调用它。 - 但在创建对象之前,JVM 怎么知道该如何初始化?
- 所以,将
main设为static,JVM 就可以直接通过类名.main()来调用,而无需实例化对象。
- 如果
void(返回值):- 含义: 空,不返回任何值。
- 原因: 当
main方法执行完毕,意味着主线程结束,Java 程序也就退出了。- 它不需要向 JVM 返回数据。
- 如果程序需要向操作系统报告状态(比如成功还是失败),通常使用
System.exit(int status),而不是通过方法返回值。
main(方法名):- 含义: 主要的。
- 原因: 这是 Java 规范硬性规定的名称。JVM 在启动时,就是死板地去寻找叫
main的方法。不能叫Main,也不能叫start。
String[] args(参数):- 含义: 一个字符串数组。
- 原因: 这是用来接收命令行参数 (Command-line arguments) 的。
- 当你通过控制台(Terminal/CMD)运行程序时,可以在类名后面跟上一些参数,这些参数会被 JVM 封装成一个
String数组传给main方法。
- 当你通过控制台(Terminal/CMD)运行程序时,可以在类名后面跟上一些参数,这些参数会被 JVM 封装成一个
使用 String[] args
示例:使用 :String[] args
很多初学者写了 args 却从来没用过。让我们看看它是怎么工作的。
public class Demo {
public static void main(String[] args) {
System.out.println("收到了 " + args.length + " 个参数");
for (int i = 0; i < args.length; i++) {
System.out.println("参数 " + i + ": " + args[i]);
}
}
}如何运行与传参:
编译:
javac Demo.java运行(带参数):
java Demo hello world 123
输出结果:
收到了 3 个参数
参数 0: hello
参数 1: world
参数 2: 123常见面试题:args 这个名字能改吗?
- 能! 它可以叫
String[] params或String[] x。只要类型是字符串数组即可。 - 格式变种: 也可以写成可变参数形式
public static void main(String... args),JVM 同样承认。
IDEA中运行代码时传参:
类似 CLI 命令行环境中的 java Main02 北京 上海 天津 tom jack

常见疑问与骚操作
常见疑问与骚操作:
main方法中可以访问静态属性和方法吗:可以。可以将它看成是一个普通的静态方法。
- 可以访问静态属性和方法。
- 不能直接访问实例属性和方法,可以通过在 main 方法中
new一个实例来访问实例属性和方法。
java【示例】main方法可以被重载 (Overload) 吗:可以。
你可以在类里写很多个叫
main的方法,只要参数列表不同。javapublic static void main(String[] args) { ... } // JVM 只认这个 public static void main(int a) { ... } // 普通方法,JVM 不理它但是,JVM 启动时只会调用那个标准的
String[] args版本。其他的main方法只是普通的静态方法,除非你在代码里手动调用它们。main方法能被其他方法调用吗:能。
它就是一个普通的静态方法。你可以在别的代码里手动调用
Demo.main(new String[]{})。当然,这很少见,容易造成逻辑混乱或死循环。Java 21 的新变化 (预览特性):
如果你使用的是 Java 21+,Java 引入了 "Unnamed Classes and Instance Main Methods" 来简化新手入门。
你不再需要写
public static void main(String[] args)这么长一串了。Java 21 简写版 (JEP 445):
javavoid main() { System.out.println("Hello, World!"); }- 不需要
public。 - 不需要
static。 - 不需要
String[] args。 - 甚至不需要包裹在
class里(如果放在未命名类文件中)。
- 不需要
总结
| 关键字 | 作用 | 能否修改? |
|---|---|---|
| public | 访问权限 | 必须是 public (Java 21 前) |
| static | 无需对象即可调用 | 必须是 static (Java 21 前) |
| void | 无返回值 | 必须是 void |
| main | 方法名 | 绝对不能改 |
| String[] | 参数类型 | 可改为 String... |
| args | 参数变量名 | 可以随意改 (如 arguments) |